"""Using broadcasts, this layer allows you to lock resources on the network.
It offers a no-handshaking, pessimistic locking scheme, for avoiding need-less
network traffic in lieu of slow response-time from the locking layer.

Problem:
Need to lock a resource on the network with the minimum amount of network
traffic. A resource here can simply be a key or a unique identifier of the
actual resource. This in some ways would be a loose lock, i.e. the locking 
layer will only ensure that whenever i get access to a resource, it is in its
latest form/shape i.e. if someone wants to lock the resource for writing, it 
doesn't need to worry about the ones who are already reading the resource
(since essentially those readers were ensured that the resource they were 
getting, at the time of their request, was in the most up-to-date form). Its 
important to note, that nothing actually of the resource is being locked by
this locking layer. Anyone, by all means, can bypass this layer and actually 
access the resource directly ... but then this locking layer would not 
guarantee any validity of the state for that resource!

1- To lock a resource:
Send a broadcast packet to inform everyone that a resource is being locked

    Problems:
    1- What if someone is already accessing this resource?
    2- What if someone doesn't get the broadcast packet?
    3- What if someone else at the same time tries to lock the resource
    
    Possible Answers:
    1a- In case if someone is accessing this resource in a way in which it 
    wouldn't change the state of the resource (i.e. it is just reading), it
    can simply ignore the lock, since it still got the latest resource at the
    time it requested for it. In case if someone is already changing the 
    resource (a state which could occur if the sender of the broadcast didn't
    receive the earlier broadcast request to lock the resource) it can just
    send an immediate message saying that the resource is already locked,
    maybe through TCP.
    
    2a- This can be solved by two methods. Firstly it can be solved by sending
    a broadcast packet n number of times ensuring that everyone receives the 
    packet (not a good solution!). The other method could be that just be
    oblivious to this possibility. The maximum that could happen is that one 
    will try to read the resource while its locked (i.e someone is trying to 
    write to it). This will be all right as long as the actually reading and 
    writing to the resource don't overlap. Another possibility is given in 1a.
    
    3a- If X sends a locking request at the same time as Y for the same 
    resource, there needs to be some way to resolve this conflict. First of 
    all, after sending a lock request, there should be a cool-off period 
    (longer than the one-way trip time of a broadcast packet over the network)
    so that contentions would become apparent. The resolution could be any 
    scheme like one with the bigger IP wins. Lets say Y is the winner. The 
    problem occurs when and if the broadcast of Y didn't reach X in the first
    place. Maybe on conflicting situations, the winner can send another 
    broadcast packet announcing its win, ensuring that the losing contenders
    get to know who has locked the resource
    
2- To unlock a resource
Send a broadcast packet informing everyone that a resource is being un-locked

    Problems:
    1- Of course, what if the broadcast doesn't reach someone who was waiting
    to lock the resource
    2- What if a process locks a resource and dies
    
    Possible Answers:
    1a,2a- The solution to both these problems is given in many NFSs. Basically
    every locking call is a lease for a certain time, and not a permanent lock.
    So if one has to lock a resource for a long time, it will continually need
    need to re-lock it before its last lock expires. 1, 2 would be solved as 
    soon as the lock expires
    
PROTOCOL
--------
1- To Lock a Resource
    a- Send broadcast packet - Format 'L "[resource key]"'
    b- Wait for broadcast conflict replies

"""
# TODO: finish Protocol documentation

import ma.log
import ma.const
import ma.net.brdcstsv
import ma.net.netutils
import re
import time
import ma.utils.timerclass as timerclass


class DistributedLock(object):
    __LOCK_TAG = 'L'
    __LOCK_INFORM_TAG = 'LI'
    __RELOCK_TAG = 'RL'
    __UNLOCK_TAG = 'U'
    __CONFLICT_WIN = 'CW'
    __BRDCST_PCKT_MSG = '%s "%s"'           # Tag followed by " and then the key and then another "
    __BRDCST_PCKT_LI_MSG = '%s "%s" %s'     # Tag followed by " and then the key and then another and then the address"
    __BRDCST_PCKT_REGEXP = r'([a-zA-Z]{1,2})\s+"(.+)"'
    __BRDCST_PCKT_REGEXP_LI_ADDY = r'(\d{,3}.\d{,3}.\d{,3}.\d{,3})?'
    __BRDCST_PCKT_WHOLE = '^\s*' + __BRDCST_PCKT_REGEXP + '\s*' + __BRDCST_PCKT_REGEXP_LI_ADDY + '\s*$'
    
    def __init__(self):
        #initialize __log for DistributedLock
        self.__log = ma.log.get_logger("ma.net")
        
        try:
            # start Broadcast listen server
            self.__log.debug('starting broadcast listen server')
            binding_net_addr, broadcast_port = ma.net.brdcstsv.BroadcastListenServer.get_standard_brdcst_bind_addr()
            self.__brdcst_srvr = ma.net.brdcstsv.BroadcastListenServer(binding_net_addr, broadcast_port, self.__received_broadcast, True, True)
            self.__brdcst_srvr.start()
            
            self.__log.debug('starting broadcast packet sender')
            broadcast_addr, self.__broadcast_port = ma.net.brdcstsv.BroadcastPacketSender.get_standard_brdcst_addr()
            self.__brdcster = ma.net.brdcstsv.BroadcastPacketSender(broadcast_addr, self.__broadcast_port)
    
            timer_interval = ma.const.XmlData.get_float_data(ma.const.xml_distlock_timer_interval)
            self.__timers = timerclass.TimerClass(timer_interval)
            self.__timers.start()
            
            # get internal ip for recording internal locks
            interface_name = ma.const.XmlData.get_str_data(ma.const.xml_network_interface)
            self.__internal_ip = ma.net.netutils.get_ip_address(interface_name)
           
            # get the time periods pertaining to the distributed lock
            self.__lock_lease_time = ma.const.XmlData.get_float_data(ma.const.xml_distlock_lease_time)
            self.__relock_wait = ma.const.XmlData.get_float_data(ma.const.xml_distlock_relock_wait)
            self.__commit_wait = ma.const.XmlData.get_float_data(ma.const.xml_distlock_commit_wait)
            self.__lock_attempt_wait = ma.const.XmlData.get_float_data(ma.const.xml_distlock_lock_reattempt_wait)
            
            # other consts
            self.__lock_disengage_wait_attempts = ma.const.XmlData.get_int_data(ma.const.xml_distlock_unlock_wait_attmpts)
            self.__lock_disengage_wait_period = ma.const.XmlData.get_float_data(ma.const.xml_distlock_unlock_wait_time)
            
            # get lock attempts value
            self.__max_lock_attempts = ma.const.XmlData.get_int_data(ma.const.xml_distlock_max_lock_attempts)
            
            # dictionary used to keep track of resources locked on the network
            self.__lock_keys_dict = {}
            
            self.__log.info('Initiated Distributed Lock')
        except Exception as err_msg:
            self.__log.error("Error while creating broadcast listen socket: %s", err_msg)
            
            
    def lock_and_hold(self, resource_key):
        """This function locks the resource key using broadcasts. Once locked 
        it will keep it locked (re-locking whenever the lock expires) until 
        unlocked
        
        This function will return as soon as the resource is successfully 
        locked or their is a timeout when the function wasnt able to lock the 
        resource. Returns True for success else False 
        
        """
        try:
            attempts = self.__max_lock_attempts
            for at in range(attempts):
                self.__log.info("Lock attempt %d for resource %s" % (at + 1, resource_key))
    
                if self.check_locked(resource_key):
                    # if already locked
                    if self.__internal_ip in self.__lock_keys_dict[resource_key]:
                        # already locked by this node
                        # if this code is not dealt ... it might be still alright since it 
                        # will be like just another node requesting for a locked resource
                        self.__log.warning("Unsuccessful lock attempt since the resource key is already locked by this node: %s" % (resource_key))
                        return False
                    else:
                        # reattempt after a while
                        self.__log.info("Unsuccessful lock attempt / resource already locked: %s" % (resource_key))
                        time.sleep(self.__lock_attempt_wait)
                        continue
                
                # send broadcast, requesting lock
                packet_msg = self.__BRDCST_PCKT_MSG % (self.__LOCK_TAG, resource_key)
                self.__brdcster.send_message(packet_msg)
                # add to list of locks
                self.__add_resource_lock(True, resource_key, self.__internal_ip)
                
                # wait for a while to gather contending packets
                time.sleep(self.__commit_wait)
                
                # now check if the added lock is still there
                if resource_key in self.__lock_keys_dict and self.__internal_ip in self.__lock_keys_dict[resource_key]:
                    # this either means that other L packets were received  or there were no contentions
                    self.__log.debug("The dictionary: %s", str(self.__lock_keys_dict))
                    
                    if len(self.__lock_keys_dict[resource_key]) > 1:
                        # if there were contenders, first see who wins 
                        win_address = self.__resolve_contention(resource_key)
                        if win_address == self.__internal_ip:
                            # announce win over the network by sending a CW broadcast
                            packet_msg = self.__BRDCST_PCKT_MSG % (self.__CONFLICT_WIN, resource_key)
                            self.__brdcster.send_message(packet_msg)
                            self.__log.info("Successfully locked resource after Winning in the Conflict: %s" % (resource_key))
                            return True
                        else:
                            # if lost over conflict fight
                            self.__log.info("Unsuccessful lock attempt: %s" % (resource_key))
                            time.sleep(self.__lock_attempt_wait)
                            continue
                    else:
                        # there were no contenders, this node is the winner
                        self.__log.info("Successfully locked resource: %s" % (resource_key))
                        return True
                else:
                    # this means either a CW, LI or RL packet was received
                    self.__log.info("Unsuccessful lock attempt: %s" % (resource_key))
                    time.sleep(self.__lock_attempt_wait)
                    continue
    
            # after all the tries, locking attempt has failed
            self.__log.warning("Unable to lock resource: %s" % (resource_key))
            return False 
        except Exception as err_msg:
            self.__log.error("Error while trying to lock a resource: %s", err_msg)
    
    
    def unlock(self, resource_key):
        """This function unlocks the resource and then returns
        """
        self.__log.debug("Attempting to unlock resource: %s", resource_key)
        
        try:
            if resource_key in self.__lock_keys_dict:
                if self.__internal_ip in self.__lock_keys_dict[resource_key]:
                    # remove from the list of locks
                    self.__remove_resource_lock(resource_key, self.__internal_ip)
            
                    # send broadcast, informing unlock of resource
                    packet_msg = self.__BRDCST_PCKT_MSG % (self.__UNLOCK_TAG, resource_key)
                    self.__brdcster.send_message(packet_msg)
            
                    self.__log.info("Successfully unlocked resource: %s", resource_key)
                else:
                    # trying to remove a lock not belonging to this node
                    self.__log.warning("Trying to unlock a resource not belonging to this node: %s" % (resource_key))
            else:
                # trying to remove a lock not belonging to this node
                self.__log.warning("Trying to unlock a resource not locked: %s", resource_key)
        except Exception as err_msg:
            self.__log.error("Error while trying to unlock a resource: %s", err_msg)
    
    
    def check_locked(self, resource_key):
        """This function checks if a certain key is locked or not. Returns 
        True if it is else False if it is available for locking"""
        ### Reader's writers problems
        if resource_key in self.__lock_keys_dict:
            return True
        else:
            return False
        
    
    def block_till_locked(self, resource_key):
        """This function blocks till a certain key is unlocked. After a certain
        number of attempts it will return a False if file is still locked, else
        it return True when the function unblocks on an unlocked of resource
        
        Note this function does not ensure that the resource will remain
        unlocked
        """
        for atmpts in range(self.__lock_disengage_wait_attempts):
            if self.check_locked(resource_key):
                time.sleep(self.__lock_disengage_wait_period)
            else:
                return True
            
        return False
    
    
    def __received_broadcast(self):
        """This function is called as a callback function by the Broadcast 
        listen server. It is called whenever a broadcast message is received
        """
        try:
            # get the last message that was received through UDP
            last_msg = self.__brdcst_srvr.msg_queue.get()
            last_msg_ip = last_msg[0]
            last_msg_pckt = last_msg[1]
            
            # split packet into the packet tag and the resource name
            pckt_split = re.findall(self.__BRDCST_PCKT_WHOLE, last_msg_pckt)
            
            pckt_correctly_recv = False
            
            # check if the packet is valid according to the regular expression
            if len(pckt_split) == 1:
                # get the valid info from the packet
                pckt_split = pckt_split[0]
                pckt_tag = pckt_split[0]
                pckt_rsrc_key = pckt_split[1]
                pckt_li_ip = pckt_split[2]
                    
                if pckt_tag != '' and pckt_rsrc_key != '':
                    if pckt_tag == self.__LOCK_TAG:
                        # deal with lock requests
                        if not self.check_locked(pckt_rsrc_key):
                            # if it was not already locked, add to list of locks
                            self.__add_resource_lock(False, pckt_rsrc_key, last_msg_ip)
                        else:
                            # if already locked, check if it was locked within the wait time period
                            max_time_passed = self.__max_time_passed_lock(pckt_rsrc_key)
                            if max_time_passed <= self.__commit_wait:
                                # if in the valid wait period, add to list of contendors for the lock
                                self.__add_resource_lock(False, pckt_rsrc_key, last_msg_ip)
                            else:
                                # send Lock Inform packet 
                                self.__send_lock_inform_packet(pckt_rsrc_key, (last_msg_ip, self.__broadcast_port))
                                
                        self.__log.info("Received a Lock packet for %s from %s" % (pckt_rsrc_key, str(last_msg_ip)))        
                        pckt_correctly_recv = True
                    elif pckt_tag == self.__LOCK_INFORM_TAG:
                        # deal with lock inform requests
                        if pckt_li_ip != '':
                            # attempt to add resource lock. Will skip if already present
                            self.__add_resource_lock(False, pckt_rsrc_key, pckt_li_ip)
                            # resolve all contentions and mark as the winning contender
                            self.__resolve_contention(pckt_rsrc_key, pckt_li_ip)
                            self.__log.info("Received a Lock Inform packet for for %s from %s"  % (pckt_rsrc_key, str(last_msg_ip)))
                            pckt_correctly_recv = True
                    elif pckt_tag == self.__RELOCK_TAG:
                        # deal with relock requests
                        if pckt_rsrc_key in self.__lock_keys_dict and last_msg_ip in self.__lock_keys_dict[pckt_rsrc_key]:
                            timer_id = self.__lock_keys_dict[pckt_rsrc_key][last_msg_ip][0]
                            # reset its timer so it can unlock at appropriate time
                            self.__timers.resetTimer(timer_id)
                        else:
                            # apparently this node didn't receive the initial lock packet
                            #    in which case add a new lock
                            self.__log.warning("Missed an original lock packet .. now locking on Re-lock packet")
                            self.__add_resource_lock(False, pckt_rsrc_key, last_msg_ip)
                            # resolve all contentions and mark as the winning contender
                            self.__resolve_contention(pckt_rsrc_key, last_msg_ip)
                        
                        self.__log.info("Received a Re-Lock packet for for %s from %s"  % (pckt_rsrc_key, str(last_msg_ip)))
                        pckt_correctly_recv = True
                    elif pckt_tag == self.__UNLOCK_TAG:
                        # deal with unlock requests
                        self.__remove_resource_lock(pckt_rsrc_key, last_msg_ip)
                        self.__log.info("Received a Un-Lock packet for for %s from %s" % (pckt_rsrc_key, str(last_msg_ip)))
                        pckt_correctly_recv = True
                    elif pckt_tag == self.__CONFLICT_WIN:
                        # deal with conflict win requests
                        # attempt to add resource lock. Will skip if already present
                        self.__add_resource_lock(False, pckt_rsrc_key, last_msg_ip)
                        # resolve all contentions and mark as the winning contender
                        self.__resolve_contention(pckt_rsrc_key, last_msg_ip)
                        self.__log.info("Received a Conflict win announcement packet for %s from %s" % (pckt_rsrc_key, str(last_msg_ip)))
                        pckt_correctly_recv = True
                
            if pckt_correctly_recv == False:
                self.__log.warning("Incorrect or corrupt packet received from %s: %s", last_msg_ip, last_msg_pckt)
                    
        except Exception as err_msg:
            self.__log.error("Error while trying to receive a broadcast packet: %s", err_msg)
    
    
    def __add_resource_lock(self, internal, resource_key, address):
        """This function does the internal processing for adding a resource
        lock to the data-structures and setting the timers accordingly.
        The <internal> value if True indicates that this is an internal lock, ie
        this system itself is locking the 
        """
        try:
            if resource_key in self.__lock_keys_dict and address in self.__lock_keys_dict[resource_key]:
                # if the given address is already present for the resource
                return
            
            if internal:
                # if internal lock, add timer for relocking  
                timer_id = self.__timers.addTimer(self.__relock_wait, self.__renew_lock_lease, [self.__timers.returnNextTimerId(), resource_key])
            else:  
                # if external lock, add timer for expiring this lock
                timer_id = self.__timers.addTimer(self.__lock_lease_time, self.__remove_resource_lock, [resource_key, address])
                
            if resource_key in self.__lock_keys_dict:
                # if adding to list of lock contenders, append the tuple
                self.__lock_keys_dict[resource_key][address] = [timer_id]
            else:
                # if adding to new key, add a list with a single tuple
                self.__lock_keys_dict[resource_key] = {address:[timer_id]}
        except Exception as err_msg:
            self.__log.error("Error while trying to add a resource lock: %s", err_msg)
    
    
    def __remove_resource_lock(self, resource_key, address):
        """This function does the internal processing for removing a resource
        lock from the data-structures and removing the timers accordingly.
        The function, by default, does not remove the complete entry for 
        resource_key from the dict rather just removes the tuple with the given 
        address. If removing the address, empties-up the list, it removes the 
        whole key from the dict
        """
        try:
            if resource_key in self.__lock_keys_dict:
                if address in self.__lock_keys_dict[resource_key]:
                    # get the timer_id for the lock that has to be removed
                    timer_id = self.__lock_keys_dict[resource_key][address][0]
                    # remove the timer
                    self.__timers.removeTimer(timer_id)
                    
                    if len(self.__lock_keys_dict[resource_key]) > 1:
                        # if there are other addresses contending for this resource
                        del self.__lock_keys_dict[resource_key][address]
                    else:
                        # if it was the only address locking this resource
                        del self.__lock_keys_dict[resource_key]
                else:
                    self.__log.warning("Attempted to remove an address which wasn't added to the resource key")
            else:
                self.__log.warning("Attempted to remove a resource key which wasn't locked")
        except Exception as err_msg:
            self.__log.error("Error while trying to remove a resource lock: %s", err_msg)
    
        
    def __check_if_resource_locked(self, resource_key):
        """This function returns true if the said resource is locked. If it is
        not it will return false...meaning it is available for locking
        """
        if resource_key in self.__lock_keys_dict:
            return True
        else:
            return False
    
    
    def __renew_lock_lease(self, timer_id, resource_key):
        """This function just broadcasts an RL packet, informing of relock
        and resets the timer
        """
        try:
            # reset the timer
            self.__timers.resetTimer(timer_id)
            # send broadcast packet for re-locking
            packet_msg = self.__BRDCST_PCKT_MSG % (self.__RELOCK_TAG, resource_key)
            self.__brdcster.send_message(packet_msg)
            self.__log.info("Sent Re-lock packet for %s", resource_key)
        except Exception as err_msg:
            self.__log.error("Error while trying to send Re-lock packet: %s", err_msg)
    
    
    def __send_lock_inform_packet(self, resource_key, recepeint_address):
        """This function sends a UDP packet informing the lock contender
        that the resource is already locked
        """
        try:
            lock_address = self.__resolve_contention(resource_key)
            packet_msg = self.__BRDCST_PCKT_LI_MSG % (self.__LOCK_INFORM_TAG, resource_key, lock_address)
            self.__brdcster.send_message(packet_msg, recepeint_address)
            self.__log.info("Sent Lock Inform packet for %s to %s", resource_key, str(recepeint_address))
        except Exception as err_msg:
            self.__log.error("Error while trying to send a Lock Inform packet: %s", err_msg)
    
    
    def __resolve_contention(self, resource_key, win_address=None):
        """This function resolves contention for a valid resource key,
        cleans up the dictionary for that key, and then returns the
        winning address. If there is only one address in that dictionary,
        that address will be returned. If an explicit address is passed,
        that is the winning address and the rest of the contenders loose
        even though their IPs may be bigger 
        """
        if resource_key in self.__lock_keys_dict:
            addresses = list(self.__lock_keys_dict[resource_key].keys())
            if win_address == None:
                max_key_addr = None
                # resolve the conflict and get the max IP
                for addr in addresses:
                    if addr > max_key_addr:
                        max_key_addr = addr
            else:
                max_key_addr = win_address
            
            # remove all other timers other than the winner IP
            for addr in addresses:
                if addr != max_key_addr:
                    timer_id = self.__lock_keys_dict[resource_key][addr][0]
                    self.__timers.removeTimer(timer_id)
                    
            # delete all other key values in the dict except the one having
            #    the highest IP
            val = self.__lock_keys_dict[resource_key][max_key_addr]
            self.__lock_keys_dict[resource_key] = {max_key_addr:val}
            return max_key_addr
        else:
            return None
    
        
    def __max_time_passed_lock(self, resource_key):
        """This function returns the maximum time passed from all contendors
        for a lock on a resource key
        """
        # check if the resouce key requested is locked
        if resource_key in self.__lock_keys_dict:
            max_time = 0
            # get all the values associated to the key
            vals = list(self.__lock_keys_dict[resource_key].values())
            for val in vals:
                # get the timer ids
                timer_id = val[0]
                # get the maximum possible time passed
                time_passed, time_passed2 = self.__timers.returnTimePassed(timer_id)
                if time_passed2 > max_time:
                    max_time = time_passed2
            
            # return the time passed after iterating through the whole dict
            return max_time
        else:
            return None
       
    
    def __del__(self):
        """Destructor"""
        try:
            self.__timers.killTimerThread()
            self.__brdcst_srvr.killServerThread()
        except Exception as err_msg:
            self.__log.error("Error while trying to destruct the DistributedLock: %s", err_msg)
        

if __name__ == '__main__':
    #main for testing
    dl = DistributedLock()
    